// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/components/login/auth/recovery/cryptohome_recovery_service_client.h"

#include <stdint.h>

#include <string>
#include <vector>

#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/notreached.h"
#include "base/strings/stringprintf.h"
#include "base/task/single_thread_task_runner.h"
#include "chromeos/ash/components/login/auth/recovery/service_constants.h"
#include "net/base/load_flags.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "url/gurl.h"

namespace ash {

namespace {

const char kAuthorizationHeaderFormat[] = "Bearer %s";
const char kProtobufContentType[] = "application/x-protobuf";

constexpr base::TimeDelta kWaitTimeout = base::Seconds(10);

constexpr net::BackoffEntry::Policy kRetryBackoffPolicy = {
    0,          // Number of initial errors to ignore.
    1 * 1000,   // Initial delay of 1 seconds in ms.
    2.0,        // Factor by which the waiting time will be multiplied.
    0.2,        // Fuzzing percentage.
    10 * 1000,  // Maximum time to delay requests in ms: 10 seconds.
    -1,         // Never discard the entry.
    false,      // Don't use initial delay.
};

constexpr int kMaxRetries = 5;

std::unique_ptr<network::ResourceRequest> GetResourceRequest(
    const GaiaAccessToken& access_token) {
  auto resource_request = std::make_unique<network::ResourceRequest>();
  resource_request->load_flags = net::LOAD_DISABLE_CACHE;
  resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
  resource_request->headers.SetHeader(
      net::HttpRequestHeaders::kAuthorization,
      base::StringPrintf(kAuthorizationHeaderFormat, access_token->c_str()));
  resource_request->headers.SetHeader(net::HttpRequestHeaders::kContentType,
                                      kProtobufContentType);
  return resource_request;
}

CryptohomeRecoveryServerStatusCode GetStatusCode(int net_error,
                                                 int response_code) {
  CryptohomeRecoveryServerStatusCode status_code;
  if (net_error != net::OK || response_code == -1) {
    status_code = CryptohomeRecoveryServerStatusCode::kNetworkError;
  } else {
    if (response_code == net::HTTP_OK) {
      status_code = CryptohomeRecoveryServerStatusCode::kSuccess;
    } else if (response_code == net::HTTP_UNAUTHORIZED) {
      status_code = CryptohomeRecoveryServerStatusCode::kAuthError;
    } else if ((response_code >= 500 && response_code < 600)) {
      status_code = CryptohomeRecoveryServerStatusCode::kServerError;
    } else {
      status_code = CryptohomeRecoveryServerStatusCode::kFatalError;
    }
  }
  return status_code;
}

bool shouldRetry(CryptohomeRecoveryServerStatusCode status_code) {
  return status_code == CryptohomeRecoveryServerStatusCode::kNetworkError ||
         status_code == CryptohomeRecoveryServerStatusCode::kServerError;
}

}  // namespace

CryptohomeRecoveryServiceClient::CryptohomeRecoveryServiceClient(
    scoped_refptr<network::SharedURLLoaderFactory> factory)
    : url_loader_factory_(factory),
      epoch_retry_backoff_(&kRetryBackoffPolicy),
      recovery_retry_backoff_(&kRetryBackoffPolicy) {
  DCHECK(url_loader_factory_);
}

CryptohomeRecoveryServiceClient::~CryptohomeRecoveryServiceClient() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}

void CryptohomeRecoveryServiceClient::FetchEpoch(
    const GaiaAccessToken& access_token,
    OnEpochResponseCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  auto resource_request = GetResourceRequest(access_token);
  resource_request->url = GetRecoveryServiceEpochURL();
  resource_request->method = net::HttpRequestHeaders::kGetMethod;

  net::NetworkTrafficAnnotationTag traffic_annotation(
      net::DefineNetworkTrafficAnnotation("cryptohome_recovery_fetch_epoch", R"(
        semantics {
          sender: "Auth session authenticator"
          description:
            "Retrieves the epoch beacon which is randomly generated by the "
            "Hardware Security Modules (HSMs). The beacon will be used in "
            "constructing and encrypting the recovery request, and ensures "
            "the request to be valid only for a short period of time."
          trigger:
            "Triggered when the user lost access to the Crytpohome due to "
            "password changes and needs to re-gain access to the Cryptohome."
          data:
            "The OAuth 2.0 access token of the account."
          destination: GOOGLE_OWNED_SERVICE
        }
        policy {
          cookies_allowed: NO
          setting:
            "Users could opt-out from Cryptohome recovery feature via 'Local "
            "data recovery' under 'Lock screen and sign-in' Settings,  which "
            "prevents generating this request."
          policy_exception_justification:
            "The feature is still under development and the relevent policy "
            "is not available yet."
        }
    )"));
  simple_url_loader_ = network::SimpleURLLoader::Create(
      std::move(resource_request), traffic_annotation);
  simple_url_loader_->SetAllowHttpErrorResults(true);
  simple_url_loader_->SetTimeoutDuration(kWaitTimeout);
  simple_url_loader_->DownloadToString(
      url_loader_factory_.get(),
      base::BindOnce(&CryptohomeRecoveryServiceClient::OnFetchEpochComplete,
                     weak_ptr_factory_.GetWeakPtr(), access_token,
                     std::move(callback)),
      network::SimpleURLLoader::kMaxBoundedStringDownloadSize);
}

void CryptohomeRecoveryServiceClient::FetchRecoveryResponse(
    const std::string& request,
    const GaiaAccessToken& access_token,
    OnRecoveryResponseCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  auto resource_request = GetResourceRequest(access_token);
  resource_request->url = GetRecoveryServiceMediateURL();
  resource_request->method = net::HttpRequestHeaders::kPostMethod;

  net::NetworkTrafficAnnotationTag traffic_annotation(
      net::DefineNetworkTrafficAnnotation(
          "cryptohome_recovery_fetch_recovery_response", R"(
        semantics {
          sender: "Auth session authenticator"
          description:
            "Retrieves the Cryptohome (per-user encrypted home directories) "
            "decryption secret from Hardware Security Modules (HSMs). The "
            "secret is used to recover user's access to the Cryptohome. All "
            "recovery attempts will be recorded in a Public Ledger (tamper "
            "resistent logging infrastructure)."
          trigger:
            "Triggered when the user lost access to the Crytpohome due to "
            "password changes and needs to re-gain access to the Cryptohome."
          data:
            "The data is encrypted using AEAD (Authenticated Encryption with "
            "Associated Data), and contains an AD (Authenticated Data) that "
            "is protected from modifications but visible in transit, and a "
            "CT (Ciphertext) that is private and only accessible by the HSMs."
            "The AD contains the user's Gaia ID, device ID (unique ID tied "
            "to the user's Cryptohome), board name and reauth proof token. "
            "The underlying plainext of the CT is the HSM's share of the "
            "secret, which the HSMs will use to perform the mediation "
            "operation and reconstitue the Cryptohome decryption secret. "
            "The request also requires OAuth 2.0 access token."
          destination: GOOGLE_OWNED_SERVICE
        }
        policy {
          cookies_allowed: NO
          setting:
            "Users could opt-out from Cryptohome recovery feature via 'Local "
            "data recovery' under 'Lock screen and sign-in' Settings,  which "
            "prevents generating this request."
          policy_exception_justification:
            "The feature is still under development and the relevent policy "
            "is not available yet."
        }
    )"));
  simple_url_loader_ = network::SimpleURLLoader::Create(
      std::move(resource_request), traffic_annotation);
  simple_url_loader_->SetAllowHttpErrorResults(true);
  simple_url_loader_->AttachStringForUpload(request, kProtobufContentType);
  simple_url_loader_->SetTimeoutDuration(kWaitTimeout);
  simple_url_loader_->DownloadToString(
      url_loader_factory_.get(),
      base::BindOnce(
          &CryptohomeRecoveryServiceClient::OnFetchRecoveryResponseComplete,
          weak_ptr_factory_.GetWeakPtr(), request, access_token,
          std::move(callback)),
      network::SimpleURLLoader::kMaxBoundedStringDownloadSize);
}

void CryptohomeRecoveryServiceClient::OnFetchEpochComplete(
    const GaiaAccessToken& access_token,
    OnEpochResponseCallback callback,
    std::unique_ptr<std::string> response_body) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  int net_error = simple_url_loader_->NetError();
  int response_code = -1;
  if (simple_url_loader_->ResponseInfo() &&
      simple_url_loader_->ResponseInfo()->headers) {
    response_code =
        simple_url_loader_->ResponseInfo()->headers->response_code();
  }
  simple_url_loader_.reset();

  CryptohomeRecoveryServerStatusCode status_code =
      GetStatusCode(net_error, response_code);
  if (status_code == CryptohomeRecoveryServerStatusCode::kSuccess) {
    epoch_retry_backoff_.InformOfRequest(true);
    std::move(callback).Run(
        CryptohomeRecoveryEpochResponse(
            std::vector<uint8_t>(response_body->begin(), response_body->end())),
        status_code);
  } else {
    epoch_retry_backoff_.InformOfRequest(false);
    LOG(ERROR) << "Error occurred during epoch request, response_code="
               << response_code << ", net_error=" << net_error
               << ", attempt=" << epoch_retry_backoff_.failure_count();
    if (!shouldRetry(status_code) ||
        epoch_retry_backoff_.failure_count() >= kMaxRetries) {
      epoch_retry_backoff_.Reset();
      std::move(callback).Run(absl::nullopt, status_code);
    } else {
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
          FROM_HERE,
          base::BindOnce(&CryptohomeRecoveryServiceClient::FetchEpoch,
                         weak_ptr_factory_.GetWeakPtr(), access_token,
                         std::move(callback)),
          epoch_retry_backoff_.GetTimeUntilRelease());
    }
  }
}

void CryptohomeRecoveryServiceClient::OnFetchRecoveryResponseComplete(
    const std::string& request_data,
    const GaiaAccessToken& access_token,
    OnRecoveryResponseCallback callback,
    std::unique_ptr<std::string> response_body) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  int net_error = simple_url_loader_->NetError();
  int response_code = -1;
  if (simple_url_loader_->ResponseInfo() &&
      simple_url_loader_->ResponseInfo()->headers) {
    response_code =
        simple_url_loader_->ResponseInfo()->headers->response_code();
  }
  simple_url_loader_.reset();

  CryptohomeRecoveryServerStatusCode status_code =
      GetStatusCode(net_error, response_code);
  if (status_code == CryptohomeRecoveryServerStatusCode::kSuccess) {
    recovery_retry_backoff_.InformOfRequest(true);
    std::move(callback).Run(CryptohomeRecoveryResponse(std::vector<uint8_t>(
                                response_body->begin(), response_body->end())),
                            status_code);
  } else {
    recovery_retry_backoff_.InformOfRequest(false);
    LOG(ERROR) << "Error occurred during recovery request, response_code="
               << response_code << ", net_error=" << net_error
               << ", attempt=" << recovery_retry_backoff_.failure_count();
    if (!shouldRetry(status_code) ||
        recovery_retry_backoff_.failure_count() >= kMaxRetries) {
      recovery_retry_backoff_.Reset();
      std::move(callback).Run(absl::nullopt, status_code);
    } else {
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
          FROM_HERE,
          base::BindOnce(
              &CryptohomeRecoveryServiceClient::FetchRecoveryResponse,
              weak_ptr_factory_.GetWeakPtr(), request_data, access_token,
              std::move(callback)),
          recovery_retry_backoff_.GetTimeUntilRelease());
    }
  }
}

}  // namespace ash
